from __future__ import print_function

import atexit
from collections import deque, namedtuple
import dateutil.parser
import errno
import itertools
from lazy import lazy
import logging
import msgpack
import os
import pkg_resources
import re
from six import iteritems, itervalues
from six.moves.urllib_parse import parse_qs
import sys
import tempfile
import time
import traceback
from uuid import uuid1 as uuidgen

from .. import Serializer, Node, Edge, QuerySyntaxError, merge_values
from ..collection import Collection, uuid_to_utc
from ..lock import Lock
from ..httpd import HTTPMethods, HTTPError, httpd, json_encode, json_decode

log = logging.getLogger(__name__)
log.addHandler(logging.NullHandler())

CACHE = {}
BLOCKSIZE=1048576
UUID = re.compile(r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$')
INT = re.compile(r'^[1-9][0-9]*$')
STRING = re.compile(r'^.+$')

def date_to_timestamp(s):
    dt = dateutil.parser.parse(s)
    return time.mktime(dt.utctimetuple()) + dt.microsecond // 1e6

def js_dumps(obj, pretty=False):
    txt = json_encode(obj)
    if pretty:
        txt += '\n'
    return txt

def mp_dumps(obj, pretty=False):
    return msgpack.packb(obj, raw=True)

Streamer = namedtuple('Streamer', 'mime encode')
streamJS = Streamer('application/json', js_dumps)
streamMP = Streamer('application/x-msgpack', mp_dumps)

Streamers = dict((x.mime, x) for x in (streamJS, streamMP))

Edge_Reserved = Edge.reserved
Node_Reserved = Node.reserved | frozenset(['depth'])

class SeedTracker(object):
    msgpack = Serializer.msgpack()
    uint = Serializer.uint()

    def __init__(self, txn):
        self.txn = txn

    @lazy
    def _seed(self):
        return self.txn.kv('lg.seeds', serialize_key=self.uint, serialize_value=self.msgpack)

    def add(self, data):
        idx = self._seed.get(0, 1)
        self._seed[idx] = data
        self._seed[0] = idx + 1

    @property
    def seeds(self):
        try:
            gen = itervalues(self._seed)
            next(gen)
        except (KeyError, StopIteration):
            return ()
        return gen

class Handler(HTTPMethods):
    content_types = {
        'application/json': json_decode,
        'application/x-msgpack': lambda x: msgpack.unpackb(x, raw=False, use_list=False),
    }
    multi = ()
    single = ()
    single_parameters = {}

    def __init__(self, collection_path=None, graph_opts=None):
        self.collection_path = collection_path
        self.graph_opts = {} if graph_opts is None else graph_opts

    @lazy
    def collection(self):
        # hackily init the collection object once per httpd worker
        # using nosync here seems to be a really good idea, since it
        # improves performance a ton, and the index is just a rebuildable
        # cache anyway. Disabling metasync doesn't appear to help significantly.
        global collection
        if collection is None:
            log.debug('worker collection init')
            collection = Collection(self.collection_path, graph_opts=self.graph_opts, nosync=True, nometasync=False)
            atexit.register(collection.close)
        return collection

    @lazy
    def lock(self):
        global lock
        return lock

    @property
    def content_type(self):
        try:
            return str(self.req.headers['Content-Type'])
        except KeyError:
            pass

    @property
    def content_length(self):
        try:
            return int(str(self.req.headers['Content-Length']))
        except:
            #if self.req.headers.contains('Transfer-Encoding', 'chunked'):
            pass

    @property
    def body(self):
        return self.req.body

    @property
    def params(self):
        return parse_qs(self.req.query, keep_blank_values=1)

    # grab latest param only
    def param(self, field, default=None):
        return self.params.get(field,[default])[-1]

    def input(self):
        try:
            encoded = b''.join(self.body)
            if len(encoded) == 0:
                return None
            unpacker = self.content_types[self.content_type]
            data = unpacker(encoded)
        except KeyError:
            raise HTTPError(409, 'Bad/missing mime type (%s) - use one of: %s' % (self.content_type, ', '.join(self.content_types.keys())))
        except ValueError:
            raise HTTPError(400, 'Decode failed for mime type: %s' % self.content_type)
        except HTTPError:
            raise
        except Exception:
            info = sys.exc_info()
            log.error('Unhandled exception: %s', traceback.print_exception(*info))
            raise HTTPError(409, 'Bad data - %s decode failed' % self.content_type)
        return data

    # called for each request
    def init(self, req, res):
        self.req = req
        self.res = res
        if req.headers.contains('Accept', 'application/x-msgpack', icase=True):
            self.streamer = Streamers['application/x-msgpack']
        else:
            self.streamer = Streamers['application/json']
        self.dumps = self.streamer.encode
        self.res.headers.set('Content-Type', self.streamer.mime)

    def format_edge(self, e):
        d = e.as_dict()
        d['src'] = { "ID": e.src.ID, "type": e.src.type, "value": e.src.value }
        d['tgt'] = { "ID": e.tgt.ID, "type": e.tgt.type, "value": e.tgt.value }
        del d['srcID']
        del d['tgtID']
        return d

    @property
    def creds(self):
        return {
            'user':  self.param('user', None),
            'roles': self.params.get('role', None),
        }

    @property
    def graphs_filter(self):
        filter = self.creds
        filter['enabled'] = True if 'enabled' in self.params else None
        filter['filters'] = self.params.get('filter', None)
        for created in ('created_after', 'created_before'):
            s = self.param(created, None)
            if s:
                try:
                    filter[created] = date_to_timestamp(s)
                except ValueError:
                    raise HTTPError(400, 'Bad datetime string for %s: %s' % (created, s))
        return filter

    def graph(self, uuid, locked=None, **kwargs):
        if locked is None:
            with self.lock.shared(uuid) as locked:
                g = self.graph(uuid, locked=locked, **kwargs)
            return g

        kwargs.update(self.creds)
        try:
            return self.collection.graph(uuid, **kwargs)
        except (IOError, OSError) as e:
            if e.errno is errno.EPERM:
                raise HTTPError(403, str(e))
            elif e.errno is errno.ENOSPC:
                raise HTTPError(507, str(e))
            raise HTTPError(404, "Backend graph for %s is inaccessible: %s" % (uuid, str(e)))

    def tmp_graph(self, uuid):
        fd, path = tempfile.mkstemp(dir=self.collection.dir, suffix=".db", prefix="tmp_%s_" % uuid)
        name = os.path.basename(path)
        dbname = name[:-3]
        return fd, name, dbname, path

    msgpack = Serializer.msgpack()
    def kv(self, txn):
        return txn.kv('lg.restobjs', serialize_value=self.msgpack)

def graphtxn(write=False, create=False, excl=False, on_success=None, on_failure=None):
    def decorator(func):
        def wrapper(self, _, uuid, *args, **kwargs):
            g = self.graph(uuid, readonly=not write, create=create, excl=excl)
            success = None
            try:
                with g.transaction(write=write) as txn:
                    lastID = txn.lastID
                    success = False
                    ret = func(self, g, txn, _, uuid, *args, **kwargs)
                    try:
                        first = next(ret)
                    except TypeError:
                        first = b'' if ret is None else ret
                        ret = ()
                    except StopIteration:
                        first = b''
                        ret = ()
                    finally:
                        if write:
                            txn.flush()
                            self.res.headers.set('X-lg-updates', txn.lastID - lastID)
                        self.res.headers.set('X-lg-maxID', txn.lastID)
                        yield first
                        for x in ret:
                            yield x
                    success = True
                # delay final chunking trailer until txn has been committed
                if write:
                    if 'x-lg-sync' in self.req.headers:
                        g.sync(force=True)
                    yield b''
            except HTTPError:
                raise
            except (IOError, OSError) as e:
                raise HTTPError(507 if e.errno is errno.ENOSPC else 500, str(e))
            except Exception as e:
                info = sys.exc_info()
                log.error('Unhandled exception: %s', traceback.print_exception(*info))
                raise e
            finally:
                if success is True:
                    if on_success:
                        on_success(g)
                elif success is False:
                    if on_failure:
                        on_failure(g)
                g.close()
        return wrapper
    return decorator


class _Input(Handler):
    def do_input(self, txn, uuid, create=False, data=None):
        if data is None:
            data = self.input()
        if data is None:
            return

        seed = data.get('seed', False)
        if seed:
            SeedTracker(txn).add(data)
        if 'meta' in data:
            self.do_meta(txn, data['meta'], seed, create)
        elif create:
            self.do_meta(txn, {}, seed, create)
        if 'chains' in data:
            self.do_chains(txn, data['chains'], seed, create)
        if 'nodes' in data:
            self.do_nodes(txn, data['nodes'], seed, create)
        if 'edges' in data:
            self.do_edges(txn, data['edges'], seed, create)

    def do_meta(self, txn, meta, seed=False, create=False):
        a = txn.nextID

        for key, val in iteritems(meta):
            txn.set(key, val, merge=True)

        b = txn.nextID
        if b > a:
            # fixme - when updating graph meta, advance any adaptor bookmarks currently at end
            pass

    def do_chains(self, txn, chains, seed, create=False):
        # cleanse & merge
        for chain in chains:
            if len(chain) % 2 == 0:
                raise HTTPError(409, 'Bad data - chain length must be odd')
            nodes = self.map_nodes(txn, chain[0::2], seed=seed, create=create)
            for i, n in enumerate(nodes):
                chain[i*2] = n
            for i, x in enumerate(chain):
                if i % 2:
                    chain[i] = self.do_edge(txn, x, src=chain[i-1], tgt=chain[i+1], seed=seed, create=create)

    def do_nodes(self, txn, nodes, seed=False, create=False):
        for n in nodes:
            self.do_node(txn, n, seed, create)

    def map_nodes(self, txn, nodes, seed=False, create=False):
        return map(lambda n: self.do_node(txn, n, seed, create), nodes)

    def do_edges(self, txn, edges, seed=False, create=False):
        for e in edges:
            self.do_edge(txn, e, seed=seed, create=create)

    def do_node(self, txn, node, seed=False, create=False):
        try:
            if create:
                del node['ID']
            param = { 'ID': node['ID'] }
        except KeyError:
            param = { 'type': node['type'], 'value': node['value'] }

        props = dict( (k, v) for k, v in iteritems(node) if k not in Node_Reserved)
        if seed:
            props['seed'] = True
        elif not create:
            props.pop('seed', None)
        param['merge'] = True
        param['properties'] = props
        try:
            return txn.node(**param)
        except TypeError:
            raise HTTPError(409, "Bad node: %s" % node)

    def _edge_node(self, txn, edge, node, seed=False, create=False, is_src=False):
        if node is None:
            node = edge['src' if is_src else 'tgt']
        if not isinstance(node, Node):
            node = self.do_node(txn, node, seed, create)
        return node

    def do_edge(self, txn, edge, src=None, tgt=None, seed=False, create=False):
        try:
            if create:
                del edge['ID']
            param = { 'ID': edge['ID'] }
        except KeyError:
            src = self._edge_node(txn, edge, src, seed=seed, create=create, is_src=True)
            tgt = self._edge_node(txn, edge, tgt, seed=seed, create=create, is_src=False)
            param = dict(type=edge['type'], value=edge.get('value', ''), src=src, tgt=tgt)
        props = dict( (k, v) for k, v in iteritems(edge) if k not in Edge_Reserved)
        if seed:
            props['seed'] = seed
        elif not create:
            props.pop('seed', None)

        try:
            e = txn.edge(**param)
        except:
            raise HTTPError(409, "Bad edge: %s" % edge)

        # handle edge costs, allowed range: [0.0, 1.0]
        # silently drop bad values:
        #  cost defaults to 1 when edge is created
        #  once cost is assigned, value may not be increased, as
        #   depth recalculation could (would?) cover the entire graph
        try:
            cost = props['cost']
            if cost < 0 or cost > 1 or cost >= e['cost']:
                del props['cost']
        except KeyError:
            pass

        e.update(props, merge=True)
        return e

    @graphtxn(write=True, create=True, excl=True, on_failure=lambda g: g.delete())
    def _create(self, g, txn, _, uuid):
        self.do_input(txn, uuid, create=True)
        self.res.code = 201
        self.res.headers.set('Location', '/graph/' + uuid)
        return self.dumps({ 'uuid': uuid, 'id': uuid }, pretty=True)

class _Streamy(object):
    def _stream_js(self, gen):
        yield '['
        try:
            x = next(gen)
            yield x
            for x in gen:
                yield ','
                yield x
        except StopIteration:
            pass
        yield ']\n'

    def stream(self, *gens):
        gen = (self.dumps(x) for x in itertools.chain.from_iterable(gens))
        if 'application/json' == self.streamer.mime:
            gen = self._stream_js(gen)
        return gen

    def _dump_json(self, txn, uuid, nodes, edges):
        yield '{"graph":"%s","id":"%s","maxID":%d,"size":%d,"created":"%s","meta":' % (uuid, uuid, txn.lastID, txn.graph.size, uuid_to_utc(uuid))
        yield self.dumps(dict(iteritems(txn)))
        yield ',"nodes":['
        try:
            n = next(nodes)
            yield self.dumps(n.as_dict())
            for n in nodes:
                yield ','
                yield self.dumps(n.as_dict())
        except StopIteration:
            pass
        yield '],"edges":['
        try:
            e = next(edges)
            yield self.dumps(self.format_edge(e))
            for e in edges:
                yield ','
                yield self.dumps(self.format_edge(e))
        except StopIteration:
            pass
        yield ']}\n'

class Graph_Root(_Input, _Streamy):
    path = ('graph',)

    def get(self, _):
        queries = self.params.get('q')
        with self.collection.context(write=False) as ctx:
            graphs = ctx.graphs(**self.graphs_filter)
            gen = self._query_graphs(graphs, queries) if queries else self.stream(graphs)
            for info in gen:
                yield info

    def _query_graphs(self, graphs, queries):
        uniq = sorted(set(queries))
        qtoc = dict((q, i) for i, q in enumerate(uniq))
        uuids = (graph['id'] for graph in graphs)
        return self.stream([uniq], self.__query_graphs(uuids, uniq, qtoc))

    def __query_graphs(self, uuids, queries, qtoc):
        for uuid in uuids:
            try:
#                with self.graph(uuid, readonly=True, create=False, hook=False) as g:
                with self.graph(uuid, create=False, hook=False) as g:
                    with g.transaction(write=False) as txn:
                        try:
                            gen = txn.mquery(queries)
                        except QuerySyntaxError as e:
                            raise HTTPError(400, str(e))

                        # fixme - should we try to make edges use the format_edge foo here?
                        for query, chain in gen:
                            yield (uuid, qtoc[query], tuple(x.as_dict() for x in chain))
            except IOError:
                pass

    def post(self, _):
        uuid = str(uuidgen())
        return self._create(None, uuid)

class Graph_UUID(_Input, _Streamy):
    path = ('graph', UUID,)
    inf = float('Inf')

    def delete(self, _, uuid):
        with self.lock.exclusive(uuid) as locked:
            # opening the graph checks user/role perms
#            with self.graph(uuid, readonly=True, create=False, locked=locked) as g:
            with self.graph(uuid, create=False, locked=locked):
                self.collection.drop(uuid)

    def put(self, _, uuid):
        target = self.collection.graph_path(uuid)
        with self.lock.exclusive(uuid):
            if os.access(target, os.F_OK):
                raise HTTPError(409, "Upload for %s failed: %s" % (uuid, 'already exists'))
            try:
                (fd, name, dbname, path) = self.tmp_graph(uuid)
            except Exception as e:
                raise HTTPError(409, "Upload for %s failed: %s" % (uuid, str(e)))

            cleanup = deque([
                lambda: os.unlink('%s-lock' % path),
                lambda: os.unlink(path),
            ])
            try:
                fh = os.fdopen(fd, 'wb')
                cleanup.appendleft(fh.close)
                while True:
                    data = self.body.read(BLOCKSIZE)
                    if len(data) == 0:
                        break
                    fh.write(data)
                cleanup.popleft()() # fh.close()
#                with self.collection.graph(dbname, readonly=True, hook=False, create=False) as g:
                with self.collection.graph(dbname, hook=False, create=False):
                    pass
                os.rename(path, target)
                cleanup.pop() # remove os.unlink(path)
                cleanup.append(lambda: os.unlink('%s-lock' % target))
            except Exception as e:
                os.unlink(name)
                raise HTTPError(409, "Upload for %s failed: %s" % (uuid, repr(e)))
            finally:
                for x in cleanup:
                    try:
                        x()
                    except:
                        pass

    def _snapshot(self, g, uuid):
        self.res.headers.set('Content-Type', 'application/octet-stream')
        self.res.headers.set('Content-Disposition','attachment; filename="%s.db"' % uuid)
        for block in g.snapshot(bs=BLOCKSIZE):
            yield block

    single = ('stop', 'start', 'limit')
    multi = ('q',)

    @property
    def stop(self):
        try:
            s = int(self.params['stop'][-1])
        except (KeyError, IndexError):
            return 0
        if s < 1:
            raise HTTPError(400, 'Bad stop parameter - if present, must be > 0')
        return s

    @property
    def start(self):
        try:
            s = int(self.params['start'][-1])
        except (KeyError, IndexError):
            return 0
        if s < 1:
            raise HTTPError(400, 'Bad start parameter - if present, must be > 0')
        return s

    @property
    def limit(self):
        return int(self.param('limit', 0))

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid):
        try:
            accept = self.req.headers['Accept'].values
        except KeyError:
            accept = ('application/json',)

        if 'application/octet-stream' in accept:
            return self._snapshot(g, uuid)

        if self.stop > 0:
            txn.beforeID = self.stop + 1

        try:
            queries = filter(bool, self.params['q'])
            if not queries:
                raise KeyError
        except KeyError:
            if self.streamer is not streamJS:
                raise HTTPError(406, 'Format for full graph dump has not been determined for non-json output')
            return self._dump_json(txn, uuid, txn.nodes(), txn.edges())

        uniq = sorted(set(queries))

        if self.param('crawl', '0') != '0':
            if self.streamer is not streamJS:
                raise HTTPError(406, 'Format for graph dump has not been determined for non-json output')
            try:
                gen = txn.mquery(uniq)
            except QuerySyntaxError as e:
                raise HTTPError(400, str(e))
            return self.spider(txn, uuid, (chain for _, chain in gen))

        qtoc = dict((q, i) for i, q in enumerate(uniq))
        try:
            gen = txn.mquery(uniq, limit=self.limit, start=self.start, stop=self.stop)
        except QuerySyntaxError as e:
            raise HTTPError(400, repr(e))
        # fixme - should we try to make edges use the format_edge foo here?
        gen = ((qtoc[query], tuple(x.as_dict() for x in chain)) for query, chain in gen)
        return self.stream([uniq], gen)

    # spider out from nodes/edges found in chain data returned from queries
    def spider(self, txn, uuid, chains):
        seenIDs = set()
        edgeIDs = deque()

        def emit_nodes_harvest_edges():
            todo = deque()
            for chain in chains:
                todo.extend(e for e in chain if e.ID not in seenIDs)
                while todo:
                    e = todo.popleft()
                    seenIDs.add(e.ID)
                    if e.is_edge:
                        # stash edges for later
                        edgeIDs.append(e.ID)
                    else:
                        # emit nodes
                        yield e
                    todo.extend(e2 for e2 in e.iterlinks(filterIDs=seenIDs))

        def emit_edges():
            for ID in edgeIDs:
                yield txn.edge(ID=ID)

        return self._dump_json(txn, uuid, emit_nodes_harvest_edges(), emit_edges())

    @graphtxn(write=False)
    def head(self, g, txn, _, uuid):
        self.res.code = 200

    @graphtxn(write=True)
    def _update(self, g, txn, _, uuid):
        self.do_input(txn, uuid)

    def post(self, _, uuid):
        if 'create' in self.params:
            # attempt create
            try:
                for x in self._create(None, uuid):
                    yield x
                return
            except HTTPError as err:
                if 404 != err.code:
                    raise

        # fall back to updating an existing
        for x in self._update(None, uuid):
            yield x

class Graph_UUID_Status(Handler):
    path = ('graph', UUID, 'status')

    def get(self, _, uuid, __):
        yield self.dumps(self.status(uuid))

    def head(self, _, uuid, __):
        self.status(uuid)
        self.res.code = 200

    def status(self, uuid):
        status = self.collection.status(uuid)
        if status is None:
            raise HTTPError(404, '%s status is not cached' % uuid)
        return status

class Reset_UUID(_Input, Handler):
    path = ('reset', UUID,)

    default_keep = {
        'seeds': 1,
        'kv': True,
    }

    def put(self, _, uuid):
        with self.lock.exclusive(uuid) as locked:
            (fd, name, dbname, path) = self.tmp_graph(uuid)
            os.close(fd)
            cleanup = [lambda: os.unlink(path)]
            try:
#                with self.graph(uuid, readonly=True, locked=locked) as g1, self.collection.graph(dbname, create=True, hook=False) as g2:
                with self.graph(uuid, locked=locked) as g1, self.collection.graph(dbname, create=True, hook=False) as g2:
                    with g1.transaction(write=False) as t1, g2.transaction(write=True) as t2:
                        # fixme
                        cleanup.append(lambda: os.unlink('%s-lock' % path))
                        keep = self.input()
                        if keep is None:
                            keep = self.default_keep

                        if keep.get('kv',False):
                            self.clone_kv(t1, t2)

                        seeds = keep.get('seeds', None)
                        if seeds:
                            self.clone_seeds(uuid, t1, t2, seeds)
                    target = g1.path
                cleanup.pop()() # unlink(path-lock)
                try:
                    # fixme
                    os.unlink('%s-lock' % target)
                except OSError:
                    pass
                os.rename(path, target)
                cleanup.pop() # unlink(path)
                # remove from index
                self.collection.remove(uuid)
                # open to re-index, bypass creds check, allow hooks to run
#                with self.collection.graph(uuid, readonly=True):
                with self.collection.graph(uuid):
                    pass
            except (IOError, OSError) as e:
                if e.errno is errno.EPERM:
                    raise HTTPError(403, str(e))
                elif e.errno is errno.ENOSPC:
                    raise HTTPError(507, str(e))
                raise HTTPError(404, "Reset failed for graph %s: %s" % (uuid, repr(e)))
            finally:
                for x in cleanup:
                    try:
                        x()
                    except:
                        pass

    def clone_kv(self, src, dst):
        try:
            s = self.kv(src)
        except KeyError:
            return
        d = self.kv(dst)
        for k, v in iteritems(s):
            d[k] = v

    def clone_seeds(self, uuid, src, dst, limit):
        i = 0
        for seed in SeedTracker(src).seeds:
            self.do_input(dst, uuid, data=seed)
            i += 1
            if i is limit:
                break

class D3_UUID(_Streamy, Handler):
    path = ('d3', UUID)

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid):
        self.map = {}
        stop = int(self.param('stop', 0))
        if stop:
            txn.beforeID = stop + 1
        return self._dump_json(txn)

    def _dump_json(self, txn):
        yield '{ "nodes":['
        nmap = {}
        nidx = 0
        nodes = txn.nodes()
        try:
            n = next(nodes)
            nmap[n.ID] = nidx
            nidx += 1
            yield self.dumps({ 'data': n.as_dict() })
            for n in nodes:
                yield ','
                nmap[n.ID] = nidx
                nidx += 1
                yield self.dumps({ 'data': n.as_dict() })
        except StopIteration:
            pass
        yield '],"edges":['
        edges = txn.edges()
        try:
            e = next(edges)
            yield self.dumps({
                'data': e.as_dict(),
                'source': nmap[e.srcID],
                'target': nmap[e.tgtID] })
            for e in edges:
                yield ','
                yield self.dumps({
                    'data': e.as_dict(),
                    'source': nmap[e.srcID],
                    'target': nmap[e.tgtID] })
        except StopIteration:
            pass
        yield ']}\n'

def exec_wrapper(code, **gvars):
    lvars = {}
    exec(code, gvars, lvars)
    return lvars

class Graph_Exec(_Input, _Streamy):
    path = ('graph', 'exec')
    eat_params = ('enabled', 'user', 'role', 'created_after', 'created_before', 'filter')

    def post(self, _, __):
        if self.content_type != 'application/python':
            raise HTTPError(409, 'Bad/missing Content-Type - must be: application/python')
        code = b''.join(self.body)
        gvars = {
            'dumps': self.dumps,
            'stream': self.stream,
            'HTTPError': HTTPError,
        }
        with self.collection.context(write=False) as ctx:
            uuids = tuple(x['id'] for x in ctx.graphs(**self.graphs_filter))

        txns_uuids = self._txns_uuids(uuids)
        try:
            lvars = exec_wrapper(code, **gvars)
            params = dict((k, v) if len(v) > 1 else (k, v[0]) for k, v in iteritems(self.params) if k not in self.eat_params)
            try:
                handler = lvars['handler']
            except KeyError:
                raise HTTPError(404, 'no handler function defined - should look like: def handler(txns_uuids, headers, **kwargs)')
            return handler(txns_uuids, self.res.headers, **params)
        except HTTPError:
            raise
        except Exception as e:
            raise HTTPError(400, 'handler raised exception: %s\n%s' % (e, code))

    def _txns_uuids(self, uuids):
        for uuid in uuids:
            try:
#                with self.graph(uuid, readonly=True, create=False, hook=False) as g:
                with self.graph(uuid, create=False, hook=False) as g:
                    with g.transaction(write=False) as txn:
                        yield txn, uuid
            except:
                pass

class Graph_UUID_Exec(_Input, _Streamy):
    path = ('graph', UUID, 'exec')
    @graphtxn(write=False)
    def post(self, g, txn, _, uuid, __):
        if self.content_type != 'application/python':
            raise HTTPError(409, 'Bad/missing Content-Type - must be: application/python')
        code = b''.join(self.body)
        gvars = {
            'dumps': self.dumps,
            'stream': self.stream,
            'HTTPError': HTTPError,
        }
        try:
            lvars = exec_wrapper(code, **gvars)
            params = dict((k, v) if len(v) > 1 else (k, v[0]) for k, v in iteritems(self.params))
            try:
                handler = lvars['handler']
            except KeyError:
                raise HTTPError(404, 'no handler function defined - should look like: def handler(txn, uuid, headers, **kwargs)')
            return handler(txn, uuid, self.res.headers, **params)
        except HTTPError:
            raise
        except Exception as e:
            raise HTTPError(400, 'handler raised exception: %s' % e)

class Graph_UUID_Meta(_Input):
    path = ('graph', UUID, 'meta')

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid, __):
        yield self.dumps(dict(iteritems(txn)), pretty=True)

    @graphtxn(write=True)
    def put(self, g, txn, _, uuid, __):
        self.do_meta(txn, self.input())

class Graph_UUID_Seeds(Handler, _Streamy):
    path = ('graph', UUID, 'seeds')
    @graphtxn(write=False)
    def get(self, g, txn, _, uuid, __):
        return self.stream(SeedTracker(txn).seeds)

class Graph_UUID_Node_ID(_Input):
    path = ('graph', UUID, 'node', INT)

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid, __, ID):
        try:
            return self.dumps(txn.node(ID=int(ID)).as_dict())
        except TypeError:
            raise HTTPError(404,"not a node")

    @graphtxn(write=True)
    def put(self, g, txn, _, uuid, __, ID):
        ID = int(ID)
        data = self.input()
        data['ID'] = ID
        self.do_node(txn, data)

class Graph_UUID_Edge_ID(_Input):
    path = ('graph', UUID, 'edge', INT)

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid, __, ID):
        try:
            return self.dumps(self.format_edge(txn.edge(ID=int(ID))))
        except TypeError:
            raise HTTPError(404, "not an edge")

    @graphtxn(write=True)
    def put(self, g, txn, _, uuid, __, ID):
        ID = int(ID)
        data = self.input()
        data['ID'] = ID
        self.do_edge(txn, data)

class KV_UUID(Handler):
    path = ('kv', UUID)

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid):
        try:
            kv = self.kv(txn)
            output = dict(iteritems(kv))
        except KeyError:
            output = {}

        yield self.dumps(output)

    @graphtxn(write=True)
    def post(self, g, txn, _, uuid):
        data = self.input()
        kv = self.kv(txn)
        try:
            gen = iteritems(data)
        except Exception as e:
            raise HTTPError(400, "bad input (%s)" % str(e))
        for k, v in gen:
            kv[k] = merge_values(kv.get(k, None), v)

    @graphtxn(write=True)
    def put(self, g, txn, _, uuid):
        data = self.input()
        kv = self.kv(txn)
        try:
            gen = iteritems(data)
        except Exception as e:
            raise HTTPError(400, "bad input (%s)" % str(e))
        for k, v in iteritems(kv):
            if k not in data:
                del kv[k]
        for k, v in gen:
            kv[k] = v

    @graphtxn(write=True)
    def delete(self, g, txn, _, uuid):
        try:
            kv = self.kv(txn)
        except KeyError:
            return
        for k in kv.iterkeys():
            del kv[k]

class KV_UUID_Key(Handler):
    path = ('kv', UUID, STRING)

    @graphtxn(write=False)
    def get(self, g, txn, _, uuid, key):
        try:
            kv = self.kv(txn)
            yield self.dumps(kv[key])
        except KeyError:
            raise HTTPError(404, 'key not found')

    @graphtxn(write=True)
    def post(self, g, txn, _, uuid, key):
        data = self.input()
        kv = self.kv(txn)
        kv[key] = merge_values(kv.get(key, None), data)

    @graphtxn(write=True)
    def put(self, g, txn, _, uuid, key):
        data = self.input()
        kv = self.kv(txn)
        kv[key] = data

    @graphtxn(write=True)
    def delete(self, g, txn, _, uuid, key):
        try:
            kv = self.kv(txn)
            del kv[key]
        except KeyError:
            raise HTTPError(404, 'key not found')

class Static(Handler):
    res = re.compile('^[^/]+\.(js|css|html)')
    mime = {
        '.html': 'text/html',
        '.css': 'text/css',
        '.js': 'text/javascript',
        '.png': 'image/png',
    }
    path = ('static', res)
    cache = {}

    def get(self, _, resource):
        try:
            body = self.cache[resource]
        except KeyError:
            body = pkg_resources.resource_string(__name__, 'data/%s' % resource)
            #self.cache[resource] = body

        extension = os.path.splitext(resource)[1]
        try:
            self.res.headers.set('Content-Type', self.mime[extension])
        except KeyError:
            pass
        self.res.headers.set('Content-Length', len(body))
        return body

class View_UUID(Static):
    path = ('view', UUID)

    def get(self, _, uuid):
        return super(View_UUID, self).get(Static.path[0], self.param('style', 'd3v4') + '.html')

class Favicon(Static):
    path = ('favicon.ico',)

    def get(self, _):
        return super(Favicon, self).get(Static.path[0], 'lemon.png')

class Server(object):
    def __init__(self, collection_path=None, graph_opts=None, **kwargs):
        classes = (
            Graph_Root,
            Graph_UUID,
            Graph_UUID_Meta,
            Graph_UUID_Node_ID,
            Graph_UUID_Edge_ID,
            Graph_Exec,
            Graph_UUID_Exec,
            Graph_UUID_Seeds,
            Graph_UUID_Status,
            Reset_UUID,
            KV_UUID,
            KV_UUID_Key,
            View_UUID,
            D3_UUID,
            Favicon,
            Static,
        )

        global lock
        lock = Lock('%s.lock' % collection_path)

        global collection
        collection = None

        handlers = tuple( H(collection_path=collection_path, graph_opts=graph_opts) for H in classes)
        kwargs['handlers'] = handlers
        httpd(**kwargs)
